use v6;
unit module JSON::Tiny;

class Actions {

method TOP($/) {
    make $<value>.made;
};
method object($/) {
    make $<pairlist>.made.hash.item;
}

method pairlist($/) {
    make $<pair>>>.made.flat;
}

method pair($/) {
    make $<string>.made => $<value>.made;
}

method array($/) {
    make $<arraylist>.made.item;
}

method arraylist($/) {
    make [$<value>.map(*.made)];
}

method string($/) {
    my $str =  +@$<str> == 1
        ?? $<str>[0].made
        !! $<str>>>.made.join;

    # see https://github.com/moritz/json/issues/25
    # when a combining character comes after an opening quote,
    # it doesn't become part of the quoted string, because
    # it's stuffed into the same grapheme as the quote.
    # so we need to extract those combining character(s)
    # from the match of the opening quote, and stuff it into the string.
    if $0.Str ne '"' {
        my @chars := $0.Str.NFC;
        $str = @chars[1..*].chrs ~ $str;
    }
    make $str
}
method value:sym<number>($/) { make +$/.Str }
method value:sym<string>($/) { make $<string>.made }
method value:sym<true>($/)   { make Bool::True  }
method value:sym<false>($/)  { make Bool::False }
method value:sym<null>($/)   { make Any }
method value:sym<object>($/) { make $<object>.made }
method value:sym<array>($/)  { make $<array>.made }

method str($/)               { make ~$/ }

my %h = '\\' => "\\",
        '/'  => "/",
        'b'  => "\b",
        'n'  => "\n",
        't'  => "\t",
        'f'  => "\f",
        'r'  => "\r",
        '"'  => "\"";
method str_escape($/) {
    if $<utf16_codepoint> {
        make utf16.new( $<utf16_codepoint>.map({:16(~$_)}) ).decode();
    } else {
        make %h{~$/};
    }
}
}



grammar Grammar {

token TOP       { \s* <value> \s* }
rule object     { '{' ~ '}' <pairlist>     }
rule pairlist   { <pair> * % \,            }
rule pair       { <string> ':' <value>     }
rule array      { '[' ~ ']' <arraylist>    }
rule arraylist  {  <value> * % [ \, ]        }

proto token value {*};
token value:sym<number> {
    '-'?
    [ 0 | <[1..9]> <[0..9]>* ]
    [ \. <[0..9]>+ ]?
    [ <[eE]> [\+|\-]? <[0..9]>+ ]?
}
token value:sym<true>    { <sym>    };
token value:sym<false>   { <sym>    };
token value:sym<null>    { <sym>    };
token value:sym<object>  { <object> };
token value:sym<array>   { <array>  };
token value:sym<string>  { <string> }

token string {
    # see https://github.com/moritz/json/issues/25
    :ignoremark
    ('"') ~ \" [ <str> | \\ <str=.str_escape> ]*
}

token str {
    <-["\\\t\x[0A]]>+
}

token str_escape {
    <["\\/bfnrt]> | 'u' <utf16_codepoint>+ % '\u'
}

token utf16_codepoint {
    <.xdigit>**4
}
}


class X::JSON::Tiny::Invalid is Exception {
    has $.source;
    method message { "Input ($.source.chars() characters) is not a valid JSON string" }
}

proto to-json($) is export {*}

multi to-json(Real:D $d) { ~$d }
multi to-json(Bool:D $d) { $d ?? 'true' !! 'false'; }
multi to-json(Str:D  $d) {
    '"'
    ~ $d.trans(['"',  '\\',   "\b", "\f", "\n", "\r", "\t"]
            => ['\"', '\\\\', '\b', '\f', '\n', '\r', '\t'])\
            .subst(/<-[\c32..\c126]>/, {
                $_.Str.encode('utf-16').values».fmt('\u%04x').join
            }, :g)
    ~ '"'
}
multi to-json(Positional:D $d) {
    return  '[ '
            ~ $d.flatmap(&to-json).join(', ')
            ~ ' ]';
}
multi to-json(Associative:D  $d) {
    return '{ '
            ~ $d.flatmap({ to-json(.key) ~ ' : ' ~ to-json(.value) }).join(', ')
            ~ ' }';
}

multi to-json(Mu:U $) { 'null' }
multi to-json(Mu:D $s) {
    die "Can't serialize an object of type " ~ $s.WHAT.perl
}

sub from-json($text) is export {
    my $a = JSON::Tiny::Actions.new();
    my $o = JSON::Tiny::Grammar.parse($text, :actions($a));
    unless $o {
        X::JSON::Tiny::Invalid.new(source => $text).throw;
    }
    
    return $o.made;
}

